旧游无处不堪寻
无寻处,惟有少年心
C Primer Plus(五)

作为程序员,不可避免地要处理大量相关数据。通常,数组能高效便捷地处理这种数据。本篇,我们就来说说 C 语言中数组的知识。

数组


C 语言的数组由数据类型相同的一系列元素组成。需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确地创建数组。

float candy[10];
char code[12];
int states[10];

方括号([])表明声明的变量是数组,方括号中的数字表明数组中的元素个数。访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。数组元素的编号从 0 开始。

初始化

C 使用如下语法来初始化数组:

int powers[8] = {1, 2, 4, 8, 16, 32, 64, 128};

用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。用数组前必须先初始化它。否则,编译器使用的值是内存相应位置上的现有值。如果初始化列表中缺少元素,则该位置上的元素为 0。
也就是说如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;但是,如果部分初始化数组,剩余的元素就会被初始化为 0。

如果初始化列表的项数多于数组元素个数,则会出现数组越界错误。
如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。

const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31 };

我们可以使用 sizeof 运算符算出数组大小。sizeof 运算符给出它的运算对象的大小(以字节为单位)。所以 sizeof days 是整个数组的大小(以字节为单位),sizeof day[0] 是数组中一个元素的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。

var size = sizeof days / sizeof days[0];

还有一种初始化数组的方法,但这种方法仅限于初始化字符数组。我们下一篇介绍。

指定初始化器

C99 增加了一个新特性: 指定初始化器(designated initializer)。利用该特性可以初始化指定的数组元素。
例如,只初始化数组中的最后一个元素。对于传统的 C 初始化语法,需要如下声明:

int arr[6] = {0, 0, 0, 0, 0, 12};

而 C99 规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素:

int arr[6] = {[5] = 12};

关于指定初始化器,比较复杂的例子如下:

#include <stdio.h>
#define MONTHS 12

int main(void) {
int days[MONTHS] = { 31, 28, [4] = 31, 30, 31, [1] = 29 };
int i;
for (i = 0; i < MONTHS; i++) {
printf("%d %d", i+1, days[i]);
}
return 0;
}

// 1 31
// 2 29
// 3 0
// 4 0
// 5 31
// 6 30
// 7 31
// 8 0
// 9 0
// 10 0
// 11 0
// 12 0

以上输出揭示了指定初始化器的两个重要特性:

  • 如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段 [4] = 31, 30, 31,那么后面这些值将被用于初始化指定元素后面的元素
  • 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化,如该例中初始化列表开始时把 days[1] 初始化为 28,但是 days[1] 又被后面的指定初始化 [1] = 29 初始化为 29

如果未指定元素大小,编译器会把数组的大小设置为足够装得下初始化的值。如:

int staff[] = {1, [6] = 4, 9, 10};

staff 数组的元素个数为 9。

数组元素赋值

声明数组后,可以借助数组下标(或索引)给数组元素赋值。如:

#include <stdio.h>
#define SIZE 50

int main(void) {
int counter, evens[SIZE];
for (counter = 0; counter < SIZE; counter++)
evens[counter] = 2 * counter;
...
}

注意:

  • C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值
  • 编译器不会检查数组下标是否使用得当。在 C 标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止

指针和数组


前面章节我们介绍过指针,指针提供一种以符号形式使用地址的方法。其实,数组表示法是在变相地使用指针。
数组名是数组首元素的地址。也就是说,如果 arr 是一个数组,下面的语句成立:

arr == &arr[0]; //数组名是数组首元素地址

arr 和 &arr[0] 两者都是常量,在程序的运行过程中,不会改变。但是,可以把它们赋值给指针变量,然后可以修改指针变量的值,需要注意指针加上一个数时,它的值如何变化。在 C 中,指针加 1 指的是增加一个存储单元。对数组而言,这意味着加 1 后的地址是下一个元素的地址。

#include <stdio.h>
#define SIZE 4

int main(void) {
short dates[SIZE];
short * pti;
short index;
double bills[SIZE];
double * ptf;
pti = dates;
ptf = bills;
printf("%23s %15s\n", "short", "double");
for (index = 0; index < SIZE; index++)
printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);
return 0;
}

// short double
// pointers + 0: 0x7fff5fbff8dc 0x7fff5fbff8a0
// pointers + 1: 0x7fff5fbff8de 0x7fff5fbff8a8
// pointers + 2: 0x7fff5fbff8e0 0x7fff5fbff8b0
// pointers + 3: 0x7fff5fbff8e2 0x7fff5fbff8b8

第 2 行打印的是两个数组开始的地址,下一行打印的是指针加 1 后的地址,即下一个元素的地址,分别观察不同类型的数组元素的地址偏移有何不同。(我们目前使用的系统中,short 类型占用 2 字节,double 类型占用 8 字节)

下面的等式体现了 C 语言的灵活性:

dates + 2 == &date[2]   // 相同的地址
*(dates + 2) == dates[2] // 相同的值

以上关系表明了数组和指针的关系十分密切,可以使用指针标识数组的元素和获得元素的值。从本质上看,同一个对象有两种表示法。实际上,C 语言标准在描述数组表示法时确实借助了指针。

数组作为函数参数

  1. 数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针
  2. 在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改

所以,数据名作为函数形参时,其全面沦落为一个普通指针!

顺带一提,不要混淆如下代码:

*(dates+2);

*dates+2;